package moodish.comm.component;

import moodish.comm.component.ClientSideMessageComponent;
import moodish.comm.ServerComm;
import moodish.comm.ServerSideMessage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;




/**
 * 
 * This is the class implementing the ServerComm Interface,
 * and also the Runnable Interface to run a thread waiting for connections.
 * 
 * @author ralso
 * @version 1
 */

public class ServerCommComponent implements ServerComm, Runnable{

	
	
	
	private static final int SERVER_PORT = 8090;
	private ServerSocket serverSocket = null;
	private ConnectedClients connectedClient = null;
	private BlockingQueue<ServerSideMessage> messagesStack = new LinkedBlockingQueue<ServerSideMessage>();

	
	/**
	 * 
	 * Starts the Moodish server. The method should setup a server socket and begin to accept connections. 
	 * This method must return immediately after a server socket has been setup, 
	 * and another thread should started to listen for connections.
	 * 
	 */
	
	@Override
	public void start() {
		
		try {
			serverSocket = new ServerSocket(SERVER_PORT);
			connectedClient = new ConnectedClients();
			messagesStack = new LinkedBlockingQueue<ServerSideMessage>();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("There was a problem creating the server socket");
		}
		new Thread(this).start();
	}
	
	
	/**
	 * 
	 * Method used to shutdown the entire server,
	 *  but before disconnects all clients connected to him.
	 * @throws IOException
	 */
	
	public void shutdown() throws IOException{
		if(!connectedClient.clients.isEmpty()){
			List<String> toDelete = new LinkedList<String>();
			
			
			for(String client : connectedClient.clients.keySet()){
				toDelete.add(client);
			}
			for(String client : toDelete){
				connectedClient.clients.get(client).disconnect();
			}
		}
		serverSocket.close();
		
		
	}
	
	
	
	
	/**
	*Get the next message received from one of the clients. 
	*If no message is has been received, the method blocks until a message has been received.
	**/
	@Override
	public ServerSideMessage getNextMessage() {
		ServerSideMessage msg = null;
		if((!(serverSocket == null)) && !(serverSocket.isClosed())){
			try {
				msg = messagesStack.take();
			} catch (InterruptedException e) {
				System.out.println("InterruptedException thrown, no message available");
			}
		}
		
		
		return msg;
		
		
	}

	 /** Checks if a message from a client is pending. If hasNextMessage() returns true,
	  *  a call to getNextMessage() will return immediately with the oldest message in the queue. 
	  *  If hasNextMessage() returns false, a call to getNextMessage() will block until a message has been received.
	  */
	
	@Override
	public boolean hasNextMessage() {
		if(messagesStack.isEmpty())
			return false;
		return true;
	}

	
	/** Send a message to a client. 
	 * This method should be used to relay messages received 
	 * from a client to a friendship. This method should be called once for each friendship.
	 */
	
	@Override
	public void sendMoodishMessage(String fromNickname, String toNickname,
			String moodishMessage) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(!(clientTo == null) && !clientTo.getClientSocket().isClosed()){
			clientTo.send(moodishMessage, fromNickname, ClientSideMessageComponent.Type.MOODISH_MESSAGE);
		}
		
		
		
	}

	/**
	 * Inform a client that another client is now a friendship.
	 */
	@Override
	public void sendNewFriendship(String toNickname, String newFriendship) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(clientTo != null && !clientTo.getClientSocket().isClosed()){
			clientTo.send(newFriendship, "SERVER", ClientSideMessageComponent.Type.FRIENDSHIP);
		}
		
		
		
		
		
		
	}

	
	/** Inform a client that a former friendship has decided to unfollow.
	 * 
	 */
	
	@Override
	public void sendNewUnfriendship(String toNickname, String unfriendship) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(clientTo != null && !clientTo.getClientSocket().isClosed()){
			clientTo.send(unfriendship, "SERVER", ClientSideMessageComponent.Type.UNFRIENDSHIP);
		}
		
		
		
		
		
		
	}

	/**
	 * Report an error to a client. An error can, for instance, be trying to follow a client
	 *  who is not connected, trying to unfollow a client not currently followed, and so on.
	 */
	@Override
	public void sendError(String toNickname, String error) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(clientTo != null && !clientTo.getClientSocket().isClosed()){
			clientTo.send(error, "SERVER",ClientSideMessageComponent.Type.ERROR );
		}
		
		
	}

	/**
	 * 
	 * Inform a client that a new client has connected. When a new client connects, 
	 * the server should call this method for each client already connected to inform 
	 * the newly connected client about the clients currently online.
	 * 
	 */
	
	@Override
	public void sendClientConnected(String toNickname, String connectedNickname) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(clientTo != null && !clientTo.getClientSocket().isClosed()){
			clientTo.send(connectedNickname, "SERVER", ClientSideMessageComponent.Type.CONNECTED);	
		}
		
		
		
	}

	/**
	 * Inform a client that another client disconnected.
	 */
	
	@Override
	public void sendClientDisconnected(String toNickname,
			String disconnectedNickname) {
		
		Client clientTo = connectedClient.searchClient(toNickname);
		if(clientTo != null && !clientTo.getClientSocket().isClosed()){
			clientTo.send(disconnectedNickname, "SERVER", ClientSideMessageComponent.Type.DISCONNECTED);
		}
		
		
	}
	
	/**
	 * Checks if a client with a certain nickname is currently connected.
	 */

	@Override
	public boolean clientIsConnected(String nickname) {

		Client isHeConnected = connectedClient.searchClient(nickname);
		return isHeConnected != null && isHeConnected.getClientSocket() != null && !isHeConnected.getClientSocket().isClosed();
	}
	
	/**
	 * Disconnect a client.
	 */

	@Override
	public void disconnectClient(String nickname) {
		if(serverSocket != null && !serverSocket.isClosed()){
			Client toBeDisconnected = connectedClient.searchClient(nickname);
			if(toBeDisconnected != null && !toBeDisconnected.getClientSocket().isClosed()){
				toBeDisconnected.disconnect();
			}else{
				System.out.println("Error disconnecting such client");
			}
		}
		
		
		
	}
	
	@Override
	public void run() {
		while(!serverSocket.isClosed()){
			try {
				Socket socketOfNewClientArrived = serverSocket.accept();
				
				ObjectInputStream inputOfNewClient = new ObjectInputStream(socketOfNewClientArrived.getInputStream());
				ObjectOutputStream outputOfNewClient = new ObjectOutputStream(socketOfNewClientArrived.getOutputStream());
				new Thread(new Client(socketOfNewClientArrived, inputOfNewClient, outputOfNewClient)).start();
				
				
			} catch (IOException e) {
				System.out.println("There was a problem waiting for a specific client, server socket will be force closed");
				
			} 
			
		}
		
	}
	
	/**
	 * Inner class - Client
	 * Each client has an associated socket, ObjectOutputStream and ObjectInputStream. Used
	 * to contact with other people
	 */
	
			class Client implements Runnable{
				
				private String nickname;
				private Socket clientSocket;
				
				
				private ObjectOutputStream out;
				private ObjectInputStream in;
				
				private Client(Socket clientSocket, ObjectInputStream in, ObjectOutputStream out){
					this.nickname = null;
					this.clientSocket = clientSocket;
					this.in = in;
					this.out = out;
				
					
				}
				
				/**
				 * Method used to disconnect a specific client from the server socket,
				 * removing it also from the Clients List. 
				 */
				
				private void disconnect() {
					
						try {
							clientSocket.close();
							connectedClient.remove(nickname);
							
						} catch (IOException e) {
							System.out.println("An IOException was thrown closing a socket because of some Thread Waiting on a I/O operation");;
						}
					
					
				}
		
				/**
				 * Method used to send ClientSideMessage throw some ObjectOutputStream 
				 * of a specific client.
				 * 
				 * @param msg
				 * @param from
				 * @param type
				 */
		
				private void send(String payload, String from, ClientSideMessageComponent.Type type) {
					if(out != null && !clientSocket.isClosed()){
						try {
							out.writeObject(new ClientSideMessageComponent(from, payload, type));
						} catch (IOException e) {
							System.out.println("There was a problem sending message to a specific client--AIAI");
						}
					}else{
						System.out.println("There was a problem trying to send the message from a specific client, maybe the ObjectOutputStream is not instantiated or the socket was already been closed.");
					}
					
				}
				
				/**
				 * Method get of clientSocket to check later if its connected.
				 * @return
				 */
				
				private Socket getClientSocket(){
					return clientSocket;
				}
		
				@Override
				public void run() {
					
					try {
						ServerSideMessage nicknameMsg = (ServerSideMessage) in.readObject();
						if(nicknameMsg.getType() == ServerSideMessage.Type.CLIENT_CONNECTED){
							this.nickname = nicknameMsg.getClientNickname();
							connectedClient.addOut(nickname, this);
							try {
								messagesStack.put(nicknameMsg);
							} catch (InterruptedException e) {
								System.out.println("There was a problem waiting on the method put of the BlockingQueue");
							}
							
						}
						
						
						
						try {
							while(!clientSocket.isClosed()){	
								ServerSideMessage msgReceived = (ServerSideMessage) in.readObject();
								messagesStack.put(msgReceived);
							}
								
						} catch (ClassNotFoundException e) {
								System.out.println("There was a problem casting the object readed to a ServerSideMessage instance");
						} catch (IOException e) {
								System.out.println("There was a problem with some client specific socket, " + nickname + " has disconnected");
								try {
									messagesStack.put(new ServerSideMessageComponent(nickname,null,ServerSideMessage.Type.CLIENT_DISCONNECTED));
								} catch (InterruptedException e1) {
									System.out.println("There was a problem waiting on the method put of the BlockingQueue to put the client disconnected message");
								}
						} catch (InterruptedException e) {
								System.out.println("There was a problem waiting on the method put of the BlockingQueue");
						}
						
					
					} catch (ClassNotFoundException e1) {
						System.out.println("There was a problem casting the object readed to a ServerSideMessage instance");
					} catch (IOException e1) {
						System.out.println("There was a problem with some client specific socket, maybe the socket as been closed");
					}
					
				}
			}
			
			/**
			 * 
			 * Synchronized Map of clients connected to the server 
			 * 
			 */
			class ConnectedClients {
				
				
				private Map<String, Client> clients = new HashMap<String, Client>();

				
				/**
				 * Method synchronized to add a new client to the list of clients connected to the
				 * server.
				 * 
				 * @param String name
				 * @param Client client
				 */
				private synchronized void addOut(String name, Client c)  {
					clients.put(name, c);
				}

				/**
				 * Method synchronized to remove a client to the list of clients connected to the
				 * server.
				 * 
				 * @param String name
				 * @throws IOException
				 */

				private synchronized void remove(String name) throws IOException {
					clients.remove(name);
				}
				
				/**
				 * Method synchronized to get a specific client from the list of clients
				 * connected to the server by his nickname.
				 * @param String name
				 * @return Client client
				 */
				private synchronized Client searchClient(String name){
					return clients.get(name);
				}
				
			}
	

}
